Rename DualSenseDriver → PlayStationDriver#2
Merged
petegordon merged 1 commit intoMay 24, 2026
Merged
Conversation
The class covers both DualSense (DS5) and DualShock 4 (DS4) USB protocols
plus their clones (GameSir Super Nova / Cyclone 2 in DS4 mode). Naming it
"DualSenseDriver" was misleading — DualSense is one specific physical
controller, not the protocol family — and users hit that confusion in
practice ("I have a DualSense, not a DualShock, why is the file/class
named like that?").
Pure rename, no behavior change:
- File: packages/core/src/drivers/{dualsense → playstation}-driver.js
(via git mv so history follows)
- Class: DualSenseDriver → PlayStationDriver
- Module header comment expanded to spell out which protocols it
covers and which clones spoof them.
Preserved on purpose:
- PROTOCOLS map key stays 'dualsense'. The visualizer's PROFILES
table uses 'dualsense' as its profile key, and entry.controllerProfile
defaults to entry.protocol → 'dualsense' for all Sony entries. Renaming
the protocol key would force a controllerProfile: 'dualsense' override
on every PlayStation entry just to keep the same GLB loading. Not
worth the churn for a cosmetic improvement. A comment in PROTOCOLS
explains the historical naming.
Touched files:
- packages/core/src/drivers/playstation-driver.js (renamed + class +
static method references DualSenseDriver._parseTouchPoint /
._crc32 updated)
- packages/core/src/devices.js (import + PROTOCOLS map entry)
- packages/core/src/index.js (barrel re-export)
- packages/core/src/drivers/controller-registry.js (one comment)
- packages/core/README.md (subpath-exports table)
- docs/ADDING-A-CONTROLLER.md (protocol-table row + the worked-example
code-link)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
petegordon
added a commit
that referenced
this pull request
Jul 8, 2026
* Support multiple Steam Controllers on one Puck (per-unit interfaces) The 2026 Steam Controller Puck is a multi-receiver: two paired bodies each stream STATE on their OWN same-vid:pid vendor interface (verified on hardware — body#1 → MI_02, body#2 → MI_03). All interfaces share the Puck's serial, so serial can't tell the bodies apart; the streaming interface is the per-unit discriminator. The old sibling-fanout model assumed ONE body behind the Puck and collapsed every interface into one logical controller, so a second body was invisible. Fixes: - core manager.claimHidDeviceForSlot: only reroute a fan-out handle to a streaming sibling when the handle isn't itself streaming — a streaming handle is already its own unit. Prevents both bodies seating onto the first streaming sibling. +2 regression tests. - overlay finishGyroConnect: same guard, so selecting the 2nd controller no longer snaps back to the 1st. - overlay overlayControllerRows: one row per STREAMING interface (was a fan:vid:pid rollup that merged both bodies); hide silent siblings; append stable #1/#2 to duplicate-named rows since the shared Puck serial can't disambiguate them. - tools/steam-multi-probe.mjs (+ npm run steam-multi-probe): node-hid diagnostic that maps each body to its Puck interface. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Wire multiple Steam Controllers into start:multi (WebHID-first boot pooling) Completes the WebHID-first migration for the multi/lobby apps: the single overlay pools every known approved HID device directly, but multi-app and lobby-app still pool via autoPoolApprovedHid(), whose "must be live in the Gamepad API" stale-pairing gate structurally excludes HID-only controllers. A Steam Controller Puck is vendor-defined HID and NEVER enumerated by the Gamepad API, so in a mixed setup (Steam + an Xbox) the present Puck was wrongly skipped at boot. - devices.js: mark both Steam Controller entries `hidOnly` (input only via WebHID, never in the Gamepad API). - manager.autoPoolApprovedHid: `hidOnly` entries bypass the Gamepad-API liveness gate — they can't satisfy it by definition. Stale/absent handles are still cleaned up by the existing phantom-eviction sweep in ingestFrame. - Tests: manager-multi-steam-claim (two HID-only bodies seat into distinct slots via ingestFrame — same frame and sequential; a single body claims exactly one seat) and manager-autopool-hidonly (gate bypass for hidOnly, unchanged skip for stale non-hidOnly). 129 core tests pass. The pseudo-pad id the manager builds for a HID claim resolves to the steam-controller profile, so the multi view swaps to the Steam model on claim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * WebHID-first: remove the Gamepad-API gate from autoPoolApprovedHid Per the intended architecture — WebHID is the primary input path, Gamepad API / XInput is only a fallback (Xbox) — boot-pooling should pool every approved, known HID device. The "must be live in the Gamepad API" gate did the opposite: it structurally dropped HID-only controllers (the Steam Controller Puck is vendor-defined HID and never appears in the Gamepad API) whenever any other Gamepad-API pad was connected. The single overlay already pooled gate-free (inline loop); multi-app and lobby-app still called the gated autoPoolApprovedHid — the migration hadn't reached them. This finishes it: - manager.autoPoolApprovedHid: gate removed; pools all approved known HID devices. Stale approved-but-absent pairings are still cleaned by the phantom-eviction sweep in ingestFrame (unchanged). - app.js: single overlay's inline pool loop converged onto the shared autoPoolApprovedHid so all three apps boot HID identically (no future drift). - devices.js: drop the interim `hidOnly` flag (only existed to bypass the now-gone gate). - tests: replace manager-autopool-hidonly with manager-autopool-webhid-first (pools regardless of Gamepad-API liveness; ignores unknown; no re-pool). 130 core tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Keep idle Steam Puck interfaces pooled (fix power-on after launch) A controller powered on AFTER the app started wasn't picked up until a restart. Cause: the phantom-eviction sweep drops any pooled HID handle that hasn't streamed within the probation window — but a Steam Puck's receiver interfaces are present as long as the dongle is plugged, just idle until a body pairs. They were pooled at boot, evicted 3s later for silence, and when a body powered on it streamed into an already-enumerated interface (no WebHID 'connect' fires), so nothing re-pooled it. Fix: exempt fan-out (needsSiblingFanout / Steam Puck) interfaces from the silence sweep in both ControllerManager.ingestFrame and the overlay's evictPhantoms. A powered-on body now streams into the still-pooled interface and claims normally. Whole-Puck removal is still handled by the hotplug 'disconnect' path. +1 test (idle Puck interface kept past probation). 131 core tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Redesign multi app: dynamic PLAYER n roster + AVAILABLE list Replaces the fixed 4-panel "Press to join" layout with a dynamic roster: - Manager pre-allocates MAX_PLAYERS (16) slots; a PLAYER n panel (its own 3D ControllerOverlay) is created only when a controller CLAIMS a slot and disposed on release, so the grid grows/shrinks with active players and GPU cost scales with real players, not the cap. Slot ordinal = player number, so numbers are sticky across a brief drop (orphan→reconnect). - Views are reconciled to slot state each frame (not via claim-time events), so a panel is correct however the slot was claimed/released; the constructor renders current state directly since the view is created the frame after the claim. - Per-player accent colors generated (golden-angle hue) for all 16. - New AVAILABLE/PLAYER roster List, toggled by a "List" button next to Debug: every claimed controller shows as PLAYER n, every pooled streaming controller as AVAILABLE; idle Steam Puck sibling interfaces are hidden; duplicate names get #1/#2. This is the "recognize power-on as AVAILABLE, button press as a player" view Pete asked for. - Empty-state prompt when no one has joined; status line updated for the dynamic model; Steam label ("Steam Controller connected"). - WebHID-first boot + periodic re-pool (RESCAN_MS) so a controller powered on after launch is picked up without a restart (belt-and-suspenders with the fan-out eviction exemption). 131 core tests pass; multi-app is app-layer (not core-tested). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Multi app: make the controller List a detached popout window The List is now an independent always-on-top window instead of an inline panel, so it can be moved/placed separately from the multi overlay (e.g. on a second monitor or off the capture area). - New HUD kind 'multi-controllers' (main.js) → multi-controllers-window.html + multi-controllers-window.js, reusing the generic detached-HUD plumbing (open-hud-window / hud-state relay) and shared hud-window-chrome.js for drag/close — same pattern as the single overlay's Controllers window. - The List button now opens the popout; multi-app forwards rosterRows() each frame via sendHudState('multi-controllers', …) (throttled ~5Hz; main drops it when the window is closed). Read-only: players join by pressing a button. - Removed the inline #controller-list panel + CSS from multi.html. Renders AVAILABLE (pooled streaming controllers) vs PLAYER n (claimed), with live in-use dots and #1/#2 disambiguation, matching the inline version. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Multi app: add close button + fix panel deformation on grid reflow - Close button: a red × in the top-right corner of the frameless multi window quits the app (window.electronAPI.quit → app.quit). List/Debug shift left to make room. - Reflow deformation: when a player joins/leaves, the auto-fit grid resizes every other panel's canvas (CSS) but not its WebGL drawing buffer, so the 3D model stretched until the window was manually nudged. Each SlotView now holds a ResizeObserver on its canvas and resizes the renderer the instant the panel reflows (setSize(w,h,false) leaves CSS size untouched, so no observer loop); disconnected on dispose. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Lobby: close button + raise player cap; share MAX_CONTROLLERS from core Two lobby fixes plus a small core refactor: - Fifth controller couldn't go ACTIVE: the lobby hardcoded 4 manager slots (P1-P4), so only 4 controllers could be recognized. Modes still cap SEATS at 4 (versus 2v2), but a 5th+ controller now becomes ACTIVE and waits in the lounge for a seat to open (the lounge + takeSeat already supported this). - Added a close (×) button in the lobby's top-right pairbar → app.quit. - Hoisted the "16" cap into core as `MAX_CONTROLLERS` + a `playerSlotIds(n)` helper (exported from the barrel), and switched both the multi app and lobby to use them instead of each hardcoding the number. 131 core tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Lobby: widen controller panel, hide idle Puck siblings; centralize the filter Addresses the lobby list showing the Steam Puck's idle receiver interfaces as phantom "AVAILABLE" rows (a side effect of keeping them pooled for power-on), and the "(via Puck)" name getting truncated. - Widened the lobby #ctrl-panel 300→380px so "Steam Controller 2026 (via Puck)" fits without ellipsis. - Centralized the "only show streaming fan-out interfaces" filter into core as `isPresentableEntry(entry)` + `ControllerManager.presentablePoolEntries()` (barrel-exported), replacing the predicate that was duplicated in the single overlay list, multi roster, lobby list, and lobby count. The driver still declares the `needsSiblingFanout` trait; core owns combining it with the entry's streaming state (the entry lives in the manager). - All four call sites now use the shared helper; the lobby count matches the filtered list. +4 core tests. 135 core tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW * Pair button: environment-aware, no redundant requestDevice stall Clicking "Pair controller" when everything was already paired ran a full navigator.hid.requestDevice() scan — which in Electron enumerates every system HID device and briefly blocks the app — just to conclude "nothing new". - core: connectHidForSlot(slotId, { prompt = true }) gates the requestDevice fallback. prompt:false runs only the cheap getDevices-pool phase (no scan). Default true keeps existing callers unchanged. +3 tests. - lobby pairController is now environment-aware: • Browser — requestDevice IS the only way to grant access → always prompt. • Electron — pool an already-approved device cheaply; only run the blocking scan when nothing is paired yet (first-time setup) or the user clicks again to force it ("✓ N connected — click again to scan for a new one"). - single overlay: connectControllerGyro(allowRequest=true); the boot auto-connect timer now passes false so a gesture-less requestDevice (which just throws, and in Electron would run the blocking scan) is never attempted at boot. The Connect button and gyro toggle keep prompting. (This flow already avoided the redundant stall by matching any granted gyro device first, so no behavior change for the user's Connect click.) 138 core tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Qnt4svMm6b8zkWfSk75vW --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Pure rename, no behavior change. The class historically named
DualSenseDriveractually covers both PlayStation HID protocols — DualShock 4 (DS4) and DualSense (DS5) — and any clones that spoof them (GameSir Super Nova / Cyclone 2 in DS4 mode). The old name was misleading: DualSense is one specific physical controller, not the protocol family. Naming the classPlayStationDrivermatches what it actually does.packages/core/src/drivers/dualsense-driver.js→playstation-driver.js(viagit mvso history follows)DualSenseDriver→PlayStationDriverdevices.js,index.js,controller-registry.js, the core README, and the onboarding doc updatedPreserved on purpose: the
PROTOCOLSmap key stays'dualsense'. The visualizer'sPROFILEStable uses'dualsense'as a profile key, and entries fall back toentry.protocolfor the visualizer profile lookup. Renaming the protocol key would force acontrollerProfile: 'dualsense'override on every PlayStation entry just to keep the same GLB loading. Not worth the churn — a comment inPROTOCOLSexplains the history.Base branch
This PR stacks on top of #1 (device dictionary + Test Report wizard). Merge that first; this one will become a clean diff against
mainafterwards.Test plan
npm installfrom repo root completes.node --input-type=module -e "import('@usersfirst/controller-core').then(m => console.log(Object.keys(m).filter(k => k.includes('Driver'))))"listsPlayStationDriver(notDualSenseDriver).🤖 Generated with Claude Code